1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.time;
18  
19  import java.io.IOException;
20  import java.io.ObjectInputStream;
21  import java.io.Serializable;
22  import java.text.DateFormatSymbols;
23  import java.text.ParseException;
24  import java.text.ParsePosition;
25  import java.util.ArrayList;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.SortedMap;
33  import java.util.TimeZone;
34  import java.util.TreeMap;
35  import java.util.concurrent.ConcurrentHashMap;
36  import java.util.concurrent.ConcurrentMap;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  
40  /**
41   * <p>FastDateParser is a fast and thread-safe version of
42   * {@link java.text.SimpleDateFormat}.</p>
43   *
44   * <p>To obtain a proxy to a FastDateParser, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} 
45   * or another variation of the factory methods of {@link FastDateFormat}.</p>
46   * 
47   * <p>Since FastDateParser is thread safe, you can use a static member instance:</p>
48   * <code>
49   *     private static final DateParser DATE_PARSER = FastDateFormat.getInstance("yyyy-MM-dd");
50   * </code>
51   * 
52   * <p>This class can be used as a direct replacement for
53   * <code>SimpleDateFormat</code> in most parsing situations.
54   * This class is especially useful in multi-threaded server environments.
55   * <code>SimpleDateFormat</code> is not thread-safe in any JDK version,
56   * nor will it be as Sun has closed the
57   * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4228335">bug</a>/RFE.
58   * </p>
59   *
60   * <p>Only parsing is supported by this class, but all patterns are compatible with
61   * SimpleDateFormat.</p>
62   *
63   * <p>The class operates in lenient mode, so for example a time of 90 minutes is treated as 1 hour 30 minutes.</p>
64   *
65   * <p>Timing tests indicate this class is as about as fast as SimpleDateFormat
66   * in single thread applications and about 25% faster in multi-thread applications.</p>
67   *
68   * @version $Id$
69   * @since 3.2
70   * @see FastDatePrinter
71   */
72  public class FastDateParser implements DateParser, Serializable {
73      /**
74       * Required for serialization support.
75       *
76       * @see java.io.Serializable
77       */
78      private static final long serialVersionUID = 2L;
79  
80      static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP");
81  
82      // defining fields
83      private final String pattern;
84      private final TimeZone timeZone;
85      private final Locale locale;
86      private final int century;
87      private final int startYear;
88  
89      // derived fields
90      private transient Pattern parsePattern;
91      private transient Strategy[] strategies;
92  
93      // dynamic fields to communicate with Strategy
94      private transient String currentFormatField;
95      private transient Strategy nextStrategy;
96  
97      /**
98       * <p>Constructs a new FastDateParser.</p>
99       * 
100      * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the 
101      * factory methods of {@link FastDateFormat} to get a cached FastDateParser instance.
102      *
103      * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
104      *  pattern
105      * @param timeZone non-null time zone to use
106      * @param locale non-null locale
107      */
108     protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
109         this(pattern, timeZone, locale, null);
110     }
111 
112     /**
113      * <p>Constructs a new FastDateParser.</p>
114      *
115      * @param pattern non-null {@link java.text.SimpleDateFormat} compatible
116      *  pattern
117      * @param timeZone non-null time zone to use
118      * @param locale non-null locale
119      * @param centuryStart The start of the century for 2 digit year parsing
120      *
121      * @since 3.3
122      */
123     protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
124         this.pattern = pattern;
125         this.timeZone = timeZone;
126         this.locale = locale;
127 
128         final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
129         int centuryStartYear;
130         if(centuryStart!=null) {
131             definingCalendar.setTime(centuryStart);
132             centuryStartYear= definingCalendar.get(Calendar.YEAR);
133         }
134         else if(locale.equals(JAPANESE_IMPERIAL)) {
135             centuryStartYear= 0;
136         }
137         else {
138             // from 80 years ago to 20 years from now
139             definingCalendar.setTime(new Date());
140             centuryStartYear= definingCalendar.get(Calendar.YEAR)-80;
141         }
142         century= centuryStartYear / 100 * 100;
143         startYear= centuryStartYear - century;
144 
145         init(definingCalendar);
146     }
147 
148     /**
149      * Initialize derived fields from defining fields.
150      * This is called from constructor and from readObject (de-serialization)
151      *
152      * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser
153      */
154     private void init(final Calendar definingCalendar) {
155 
156         final StringBuilder regex= new StringBuilder();
157         final List<Strategy> collector = new ArrayList<Strategy>();
158 
159         final Matcher patternMatcher= formatPattern.matcher(pattern);
160         if(!patternMatcher.lookingAt()) {
161             throw new IllegalArgumentException(
162                     "Illegal pattern character '" + pattern.charAt(patternMatcher.regionStart()) + "'");
163         }
164 
165         currentFormatField= patternMatcher.group();
166         Strategy currentStrategy= getStrategy(currentFormatField, definingCalendar);
167         for(;;) {
168             patternMatcher.region(patternMatcher.end(), patternMatcher.regionEnd());
169             if(!patternMatcher.lookingAt()) {
170                 nextStrategy = null;
171                 break;
172             }
173             final String nextFormatField= patternMatcher.group();
174             nextStrategy = getStrategy(nextFormatField, definingCalendar);
175             if(currentStrategy.addRegex(this, regex)) {
176                 collector.add(currentStrategy);
177             }
178             currentFormatField= nextFormatField;
179             currentStrategy= nextStrategy;
180         }
181         if (patternMatcher.regionStart() != patternMatcher.regionEnd()) {
182             throw new IllegalArgumentException("Failed to parse \""+pattern+"\" ; gave up at index "+patternMatcher.regionStart());
183         }
184         if(currentStrategy.addRegex(this, regex)) {
185             collector.add(currentStrategy);
186         }
187         currentFormatField= null;
188         strategies= collector.toArray(new Strategy[collector.size()]);
189         parsePattern= Pattern.compile(regex.toString());
190     }
191 
192     // Accessors
193     //-----------------------------------------------------------------------
194     /* (non-Javadoc)
195      * @see org.apache.commons.lang3.time.DateParser#getPattern()
196      */
197     @Override
198     public String getPattern() {
199         return pattern;
200     }
201 
202     /* (non-Javadoc)
203      * @see org.apache.commons.lang3.time.DateParser#getTimeZone()
204      */
205     @Override
206     public TimeZone getTimeZone() {
207         return timeZone;
208     }
209 
210     /* (non-Javadoc)
211      * @see org.apache.commons.lang3.time.DateParser#getLocale()
212      */
213     @Override
214     public Locale getLocale() {
215         return locale;
216     }
217 
218     /**
219      * Returns the generated pattern (for testing purposes).
220      *
221      * @return the generated pattern
222      */
223     Pattern getParsePattern() {
224         return parsePattern;
225     }
226 
227     // Basics
228     //-----------------------------------------------------------------------
229     /**
230      * <p>Compare another object for equality with this object.</p>
231      *
232      * @param obj  the object to compare to
233      * @return <code>true</code>if equal to this instance
234      */
235     @Override
236     public boolean equals(final Object obj) {
237         if (! (obj instanceof FastDateParser) ) {
238             return false;
239         }
240         final FastDateParser other = (FastDateParser) obj;
241         return pattern.equals(other.pattern)
242             && timeZone.equals(other.timeZone)
243             && locale.equals(other.locale);
244     }
245 
246     /**
247      * <p>Return a hashcode compatible with equals.</p>
248      *
249      * @return a hashcode compatible with equals
250      */
251     @Override
252     public int hashCode() {
253         return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
254     }
255 
256     /**
257      * <p>Get a string version of this formatter.</p>
258      *
259      * @return a debugging string
260      */
261     @Override
262     public String toString() {
263         return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]";
264     }
265 
266     // Serializing
267     //-----------------------------------------------------------------------
268     /**
269      * Create the object after serialization. This implementation reinitializes the
270      * transient properties.
271      *
272      * @param in ObjectInputStream from which the object is being deserialized.
273      * @throws IOException if there is an IO issue.
274      * @throws ClassNotFoundException if a class cannot be found.
275      */
276     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
277         in.defaultReadObject();
278 
279         final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
280         init(definingCalendar);
281     }
282 
283     /* (non-Javadoc)
284      * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String)
285      */
286     @Override
287     public Object parseObject(final String source) throws ParseException {
288         return parse(source);
289     }
290 
291     /* (non-Javadoc)
292      * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String)
293      */
294     @Override
295     public Date parse(final String source) throws ParseException {
296         final Date date= parse(source, new ParsePosition(0));
297         if(date==null) {
298             // Add a note re supported date range
299             if (locale.equals(JAPANESE_IMPERIAL)) {
300                 throw new ParseException(
301                         "(The " +locale + " locale does not support dates before 1868 AD)\n" +
302                                 "Unparseable date: \""+source+"\" does not match "+parsePattern.pattern(), 0);
303             }
304             throw new ParseException("Unparseable date: \""+source+"\" does not match "+parsePattern.pattern(), 0);
305         }
306         return date;
307     }
308 
309     /* (non-Javadoc)
310      * @see org.apache.commons.lang3.time.DateParser#parseObject(java.lang.String, java.text.ParsePosition)
311      */
312     @Override
313     public Object parseObject(final String source, final ParsePosition pos) {
314         return parse(source, pos);
315     }
316 
317     /**
318      * This implementation updates the ParsePosition if the parse succeeeds.
319      * However, unlike the method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)}
320      * it is not able to set the error Index - i.e. {@link ParsePosition#getErrorIndex()} -  if the parse fails.
321      * <p>
322      * To determine if the parse has succeeded, the caller must check if the current parse position
323      * given by {@link ParsePosition#getIndex()} has been updated. If the input buffer has been fully
324      * parsed, then the index will point to just after the end of the input buffer.
325      *
326      * @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String, java.text.ParsePosition)
327      * {@inheritDoc}
328      */
329     @Override
330     public Date parse(final String source, final ParsePosition pos) {
331         final int offset= pos.getIndex();
332         final Matcher matcher= parsePattern.matcher(source.substring(offset));
333         if(!matcher.lookingAt()) {
334             return null;
335         }
336         // timing tests indicate getting new instance is 19% faster than cloning
337         final Calendar cal= Calendar.getInstance(timeZone, locale);
338         cal.clear();
339 
340         for(int i=0; i<strategies.length;) {
341             final Strategy strategy= strategies[i++];
342             strategy.setCalendar(this, cal, matcher.group(i));
343         }
344         pos.setIndex(offset+matcher.end());
345         return cal.getTime();
346     }
347 
348     // Support for strategies
349     //-----------------------------------------------------------------------
350 
351     /**
352      * Escape constant fields into regular expression
353      * @param regex The destination regex
354      * @param value The source field
355      * @param unquote If true, replace two success quotes ('') with single quote (')
356      * @return The <code>StringBuilder</code>
357      */
358     private static StringBuilder escapeRegex(final StringBuilder regex, final String value, final boolean unquote) {
359         regex.append("\\Q");
360         for(int i= 0; i<value.length(); ++i) {
361             char c= value.charAt(i);
362             switch(c) {
363             case '\'':
364                 if(unquote) {
365                     if(++i==value.length()) {
366                         return regex;
367                     }
368                     c= value.charAt(i);
369                 }
370                 break;
371             case '\\':
372                 if(++i==value.length()) {
373                     break;
374                 }
375                 /*
376                  * If we have found \E, we replace it with \E\\E\Q, i.e. we stop the quoting,
377                  * quote the \ in \E, then restart the quoting.
378                  *
379                  * Otherwise we just output the two characters.
380                  * In each case the initial \ needs to be output and the final char is done at the end
381                  */
382                 regex.append(c); // we always want the original \
383                 c = value.charAt(i); // Is it followed by E ?
384                 if (c == 'E') { // \E detected
385                   regex.append("E\\\\E\\"); // see comment above
386                   c = 'Q'; // appended below
387                 }
388                 break;
389             default:
390                 break;
391             }
392             regex.append(c);
393         }
394         regex.append("\\E");
395         return regex;
396     }
397 
398 
399     /**
400      * Get the short and long values displayed for a field
401      * @param field The field of interest
402      * @param definingCalendar The calendar to obtain the short and long values
403      * @param locale The locale of display names
404      * @return A Map of the field key / value pairs
405      */
406     private static Map<String, Integer> getDisplayNames(final int field, final Calendar definingCalendar, final Locale locale) {
407         return definingCalendar.getDisplayNames(field, Calendar.ALL_STYLES, locale);
408     }
409 
410     /**
411      * Adjust dates to be within appropriate century
412      * @param twoDigitYear The year to adjust
413      * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive)
414      */
415     private int adjustYear(final int twoDigitYear) {
416         final int trial= century + twoDigitYear;
417         return twoDigitYear>=startYear ?trial :trial+100;
418     }
419 
420     /**
421      * Is the next field a number?
422      * @return true, if next field will be a number
423      */
424     boolean isNextNumber() {
425         return nextStrategy!=null && nextStrategy.isNumber();
426     }
427 
428     /**
429      * What is the width of the current field?
430      * @return The number of characters in the current format field
431      */
432     int getFieldWidth() {
433         return currentFormatField.length();
434     }
435 
436     /**
437      * A strategy to parse a single field from the parsing pattern
438      */
439     private static abstract class Strategy {
440         
441         /**
442          * Is this field a number?
443          * The default implementation returns false.
444          *
445          * @return true, if field is a number
446          */
447         boolean isNumber() {
448             return false;
449         }
450         
451         /**
452          * Set the Calendar with the parsed field.
453          *
454          * The default implementation does nothing.
455          *
456          * @param parser The parser calling this strategy
457          * @param cal The <code>Calendar</code> to set
458          * @param value The parsed field to translate and set in cal
459          */
460         void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
461 
462         }
463         
464         /**
465          * Generate a <code>Pattern</code> regular expression to the <code>StringBuilder</code>
466          * which will accept this field
467          * @param parser The parser calling this strategy
468          * @param regex The <code>StringBuilder</code> to append to
469          * @return true, if this field will set the calendar;
470          * false, if this field is a constant value
471          */
472         abstract boolean addRegex(FastDateParser parser, StringBuilder regex);
473 
474     }
475 
476     /**
477      * A <code>Pattern</code> to parse the user supplied SimpleDateFormat pattern
478      */
479     private static final Pattern formatPattern= Pattern.compile(
480             "D+|E+|F+|G+|H+|K+|M+|S+|W+|X+|Z+|a+|d+|h+|k+|m+|s+|w+|y+|z+|''|'[^']++(''[^']*+)*+'|[^'A-Za-z]++");
481 
482     /**
483      * Obtain a Strategy given a field from a SimpleDateFormat pattern
484      * @param formatField A sub-sequence of the SimpleDateFormat pattern
485      * @param definingCalendar The calendar to obtain the short and long values
486      * @return The Strategy that will handle parsing for the field
487      */
488     private Strategy getStrategy(final String formatField, final Calendar definingCalendar) {
489         switch(formatField.charAt(0)) {
490         case '\'':
491             if(formatField.length()>2) {
492                 return new CopyQuotedStrategy(formatField.substring(1, formatField.length()-1));
493             }
494             //$FALL-THROUGH$
495         default:
496             return new CopyQuotedStrategy(formatField);
497         case 'D':
498             return DAY_OF_YEAR_STRATEGY;
499         case 'E':
500             return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
501         case 'F':
502             return DAY_OF_WEEK_IN_MONTH_STRATEGY;
503         case 'G':
504             return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
505         case 'H':  // Hour in day (0-23)
506             return HOUR_OF_DAY_STRATEGY;
507         case 'K':  // Hour in am/pm (0-11) 
508             return HOUR_STRATEGY;
509         case 'M':
510             return formatField.length()>=3 ?getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) :NUMBER_MONTH_STRATEGY;
511         case 'S':
512             return MILLISECOND_STRATEGY;
513         case 'W':
514             return WEEK_OF_MONTH_STRATEGY;
515         case 'a':
516             return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
517         case 'd':
518             return DAY_OF_MONTH_STRATEGY;
519         case 'h':  // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0
520             return HOUR12_STRATEGY;
521         case 'k':  // Hour in day (1-24), i.e. midnight is 24, not 0
522             return HOUR24_OF_DAY_STRATEGY;
523         case 'm':
524             return MINUTE_STRATEGY;
525         case 's':
526             return SECOND_STRATEGY;
527         case 'w':
528             return WEEK_OF_YEAR_STRATEGY;
529         case 'y':
530             return formatField.length()>2 ?LITERAL_YEAR_STRATEGY :ABBREVIATED_YEAR_STRATEGY;
531         case 'X':
532             return ISO8601TimeZoneStrategy.getStrategy(formatField.length());
533         case 'Z':
534             if (formatField.equals("ZZ")) {
535                 return ISO_8601_STRATEGY;
536             }
537             //$FALL-THROUGH$
538         case 'z':
539             return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
540         }
541     }
542 
543     @SuppressWarnings("unchecked") // OK because we are creating an array with no entries
544     private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT];
545 
546     /**
547      * Get a cache of Strategies for a particular field
548      * @param field The Calendar field
549      * @return a cache of Locale to Strategy
550      */
551     private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
552         synchronized(caches) {
553             if(caches[field]==null) {
554                 caches[field]= new ConcurrentHashMap<Locale,Strategy>(3);
555             }
556             return caches[field];
557         }
558     }
559 
560     /**
561      * Construct a Strategy that parses a Text field
562      * @param field The Calendar field
563      * @param definingCalendar The calendar to obtain the short and long values
564      * @return a TextStrategy for the field and Locale
565      */
566     private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
567         final ConcurrentMap<Locale,Strategy> cache = getCache(field);
568         Strategy strategy= cache.get(locale);
569         if(strategy==null) {
570             strategy= field==Calendar.ZONE_OFFSET
571                     ? new TimeZoneStrategy(locale)
572                     : new CaseInsensitiveTextStrategy(field, definingCalendar, locale);
573             final Strategy inCache= cache.putIfAbsent(locale, strategy);
574             if(inCache!=null) {
575                 return inCache;
576             }
577         }
578         return strategy;
579     }
580 
581     /**
582      * A strategy that copies the static or quoted field in the parsing pattern
583      */
584     private static class CopyQuotedStrategy extends Strategy {
585         private final String formatField;
586 
587         /**
588          * Construct a Strategy that ensures the formatField has literal text
589          * @param formatField The literal text to match
590          */
591         CopyQuotedStrategy(final String formatField) {
592             this.formatField= formatField;
593         }
594 
595         /**
596          * {@inheritDoc}
597          */
598         @Override
599         boolean isNumber() {
600             char c= formatField.charAt(0);
601             if(c=='\'') {
602                 c= formatField.charAt(1);
603             }
604             return Character.isDigit(c);
605         }
606 
607         /**
608          * {@inheritDoc}
609          */
610         @Override
611         boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
612             escapeRegex(regex, formatField, true);
613             return false;
614         }
615     }
616 
617     /**
618      * A strategy that handles a text field in the parsing pattern
619      */
620      private static class CaseInsensitiveTextStrategy extends Strategy {
621         private final int field;
622         private final Locale locale;
623         private final Map<String, Integer> lKeyValues;
624 
625         /**
626          * Construct a Strategy that parses a Text field
627          * @param field  The Calendar field
628          * @param definingCalendar  The Calendar to use
629          * @param locale  The Locale to use
630          */
631         CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
632             this.field= field;
633             this.locale= locale;
634             final Map<String, Integer> keyValues = getDisplayNames(field, definingCalendar, locale);
635             this.lKeyValues= new HashMap<String,Integer>();
636 
637             for(final Map.Entry<String, Integer> entry : keyValues.entrySet()) {
638                 lKeyValues.put(entry.getKey().toLowerCase(locale), entry.getValue());
639             }
640         }
641 
642         /**
643          * {@inheritDoc}
644          */
645         @Override
646         boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
647             regex.append("((?iu)");
648             for(final String textKeyValue : lKeyValues.keySet()) {
649                 escapeRegex(regex, textKeyValue, false).append('|');
650             }
651             regex.setCharAt(regex.length()-1, ')');
652             return true;
653         }
654 
655         /**
656          * {@inheritDoc}
657          */
658         @Override
659         void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
660             final Integer iVal = lKeyValues.get(value.toLowerCase(locale));
661             if(iVal == null) {
662                 final StringBuilder sb= new StringBuilder(value);
663                 sb.append(" not in (");
664                 for(final String textKeyValue : lKeyValues.keySet()) {
665                     sb.append(textKeyValue).append(' ');
666                 }
667                 sb.setCharAt(sb.length()-1, ')');
668                 throw new IllegalArgumentException(sb.toString());
669             }
670             cal.set(field, iVal.intValue());
671         }
672     }
673 
674 
675     /**
676      * A strategy that handles a number field in the parsing pattern
677      */
678     private static class NumberStrategy extends Strategy {
679         private final int field;
680 
681         /**
682          * Construct a Strategy that parses a Number field
683          * @param field The Calendar field
684          */
685         NumberStrategy(final int field) {
686              this.field= field;
687         }
688 
689         /**
690          * {@inheritDoc}
691          */
692         @Override
693         boolean isNumber() {
694             return true;
695         }
696 
697         /**
698          * {@inheritDoc}
699          */
700         @Override
701         boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
702             // See LANG-954: We use {Nd} rather than {IsNd} because Android does not support the Is prefix
703             if(parser.isNextNumber()) {
704                 regex.append("(\\p{Nd}{").append(parser.getFieldWidth()).append("}+)");
705             }
706             else {
707                 regex.append("(\\p{Nd}++)");
708             }
709             return true;
710         }
711 
712         /**
713          * {@inheritDoc}
714          */
715         @Override
716         void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
717             cal.set(field, modify(Integer.parseInt(value)));
718         }
719 
720         /**
721          * Make any modifications to parsed integer
722          * @param iValue The parsed integer
723          * @return The modified value
724          */
725         int modify(final int iValue) {
726             return iValue;
727         }
728     }
729 
730     private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
731         /**
732          * {@inheritDoc}
733          */
734         @Override
735         void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
736             int iValue= Integer.parseInt(value);
737             if(iValue<100) {
738                 iValue= parser.adjustYear(iValue);
739             }
740             cal.set(Calendar.YEAR, iValue);
741         }
742     };
743 
744     /**
745      * A strategy that handles a timezone field in the parsing pattern
746      */
747     private static class TimeZoneStrategy extends Strategy {
748 
749         private final String validTimeZoneChars;
750         private final SortedMap<String, TimeZone> tzNames= new TreeMap<String, TimeZone>(String.CASE_INSENSITIVE_ORDER);
751 
752         /**
753          * Index of zone id
754          */
755         private static final int ID = 0;
756         /**
757          * Index of the long name of zone in standard time
758          */
759         private static final int LONG_STD = 1;
760         /**
761          * Index of the short name of zone in standard time
762          */
763         private static final int SHORT_STD = 2;
764         /**
765          * Index of the long name of zone in daylight saving time
766          */
767         private static final int LONG_DST = 3;
768         /**
769          * Index of the short name of zone in daylight saving time
770          */
771         private static final int SHORT_DST = 4;
772 
773         /**
774          * Construct a Strategy that parses a TimeZone
775          * @param locale The Locale
776          */
777         TimeZoneStrategy(final Locale locale) {
778             final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
779             for (final String[] zone : zones) {
780                 if (zone[ID].startsWith("GMT")) {
781                     continue;
782                 }
783                 final TimeZone tz = TimeZone.getTimeZone(zone[ID]);
784                 if (!tzNames.containsKey(zone[LONG_STD])){
785                     tzNames.put(zone[LONG_STD], tz);
786                 }
787                 if (!tzNames.containsKey(zone[SHORT_STD])){
788                     tzNames.put(zone[SHORT_STD], tz);
789                 }
790                 if (tz.useDaylightTime()) {
791                     if (!tzNames.containsKey(zone[LONG_DST])){
792                         tzNames.put(zone[LONG_DST], tz);
793                     }
794                     if (!tzNames.containsKey(zone[SHORT_DST])){
795                         tzNames.put(zone[SHORT_DST], tz);
796                     }
797                 }
798             }
799 
800             final StringBuilder sb= new StringBuilder();
801             sb.append("(GMT[+-]\\d{1,2}:\\d{2}").append('|');
802             sb.append("[+-]\\d{4}").append('|');
803             for(final String id : tzNames.keySet()) {
804                 escapeRegex(sb, id, false).append('|');
805             }
806             sb.setCharAt(sb.length()-1, ')');
807             validTimeZoneChars= sb.toString();
808         }
809 
810         /**
811          * {@inheritDoc}
812          */
813         @Override
814         boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
815             regex.append(validTimeZoneChars);
816             return true;
817         }
818 
819         /**
820          * {@inheritDoc}
821          */
822         @Override
823         void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
824             TimeZone tz;
825             if(value.charAt(0)=='+' || value.charAt(0)=='-') {
826                 tz= TimeZone.getTimeZone("GMT"+value);
827             }
828             else if(value.startsWith("GMT")) {
829                 tz= TimeZone.getTimeZone(value);
830             }
831             else {
832                 tz= tzNames.get(value);
833                 if(tz==null) {
834                     throw new IllegalArgumentException(value + " is not a supported timezone name");
835                 }
836             }
837             cal.setTimeZone(tz);
838         }
839     }
840     
841     private static class ISO8601TimeZoneStrategy extends Strategy {
842         // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm 
843         private final String pattern;
844 
845         /**
846          * Construct a Strategy that parses a TimeZone
847          * @param pattern The Pattern
848          */
849         ISO8601TimeZoneStrategy(String pattern) {
850             this.pattern = pattern;
851         }
852         
853         /**
854          * {@inheritDoc}
855          */
856         @Override
857         boolean addRegex(FastDateParser parser, StringBuilder regex) {
858             regex.append(pattern);
859             return true;
860         }
861         
862         /**
863          * {@inheritDoc}
864          */
865         @Override
866         void setCalendar(FastDateParser parser, Calendar cal, String value) {
867             if (value.equals("Z")) {
868                 cal.setTimeZone(TimeZone.getTimeZone("UTC"));
869             } else {
870                 cal.setTimeZone(TimeZone.getTimeZone("GMT" + value));
871             }
872         }
873         
874         private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
875         private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
876         private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
877 
878         /**
879          * Factory method for ISO8601TimeZoneStrategies.
880          * 
881          * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
882          * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such
883          *          strategy exists, an IllegalArgumentException will be thrown.
884          */
885         static Strategy getStrategy(int tokenLen) {
886             switch(tokenLen) {
887             case 1:
888                 return ISO_8601_1_STRATEGY;
889             case 2:
890                 return ISO_8601_2_STRATEGY;
891             case 3:
892                 return ISO_8601_3_STRATEGY;
893             default:
894                 throw new IllegalArgumentException("invalid number of X");
895             }
896         }
897     }
898 
899     private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
900         @Override
901         int modify(final int iValue) {
902             return iValue-1;
903         }
904     };
905     private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
906     private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
907     private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
908     private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
909     private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
910     private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
911     private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
912     private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
913         @Override
914         int modify(final int iValue) {
915             return iValue == 24 ? 0 : iValue;
916         }
917     };
918     private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
919         @Override
920         int modify(final int iValue) {
921             return iValue == 12 ? 0 : iValue;
922         }
923     };
924     private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
925     private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
926     private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
927     private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
928     private static final Strategy ISO_8601_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::?\\d{2})?))");
929 
930 
931 }